昨天把 MCP 的 Host、Client、Server 三個角色拆開來看。
其中 Server 的工作,是把能力提供給 Client。但「提供能力」不只有 Tool。
MCP Server 最主要有三種原語:
定義單獨看都不難,真正開始設計 Server 時,比較容易卡住的是:
這個功能到底該放 Tool、Resource,還是 Prompt?
今天直接做一個最小的筆記 Server,把三種原語放在一起跑一次。
最後其實只需要記一句:
看誰控制它。
| 原語 | 主要控制者 | 用途 |
|---|---|---|
| Tools | 模型 | 模型自行判斷是否呼叫 |
| Resources | Host / Client | 應用程式決定是否讀取、載入 |
| Prompts | 使用者 | 使用者主動選擇固定任務模板 |

這次做一個很小的筆記 Server。
先放兩個 Tools:
@mcp.tool()
def add_note(
key: Annotated[str, Field(description="筆記的代號,例如 meeting")],
text: Annotated[str, Field(description="筆記內容")],
) -> str:
"""新增或覆寫一則筆記。"""
existed = key in NOTES
NOTES[key] = text
return f"{'覆寫' if existed else '新增'}筆記 {key}"
@mcp.tool()
def now(
tz: Annotated[
Literal["UTC", "Asia/Taipei"],
Field(description="時區")
] = "Asia/Taipei",
) -> str:
"""查詢目前時間。"""
再放 Resources:
@mcp.resource("notes://all", name="所有筆記", mime_type="text/plain")
def all_notes() -> str:
return "\n\n".join(
f"[{k}]\n{v}" for k, v in NOTES.items()
)
@mcp.resource("notes://{key}", name="單則筆記", mime_type="text/plain")
def one_note(key: str) -> str:
return NOTES.get(key, f"(沒有名為 {key} 的筆記)")
最後放一個 Prompt:
@mcp.prompt(name="summarize_notes", title="摘要所有筆記")
def summarize_notes(
style: Annotated[
Literal["條列", "一段話"],
Field(description="想要的摘要格式")
] = "條列",
) -> str:
return (
f"請閱讀我所有的筆記,並用「{style}」的方式摘要重點。\n"
"只講結論,不要重複原文。用繁體中文回答。"
)
三個都由同一個 MCP Server 提供,但使用方式完全不同。
昨天提過,MCP 不會取代 function calling。
所以這次直接把 MCP Server 回傳的 Tool definitions 轉成模型可以使用的 function calling 格式。
丟給模型的工具只有:
['add_note', 'now']
Resources 和 Prompts 不會出現在這份 Tool 清單裡。
接著問:
現在台北時間幾點?
模型選擇呼叫:
now({"tz": "Asia/Taipei"})
再問:
幫我記一則筆記:明天要買咖啡豆,代號 shopping
模型改成呼叫:
add_note({
"key": "shopping",
"text": "明天要買咖啡豆"
})
這裡沒有另外寫:
if 問時間:
call now()
elif 要記筆記:
call add_note()
Host 做的是把可用的 Tools 告訴模型。
至於要不要呼叫、呼叫哪一個,則由模型根據當下任務決定。
這就是 Tools 由模型控制。
剛才模型透過 add_note 新增了 shopping。
這時候再讀:
notes://all
會得到:
[meeting]
10/15 站立會議:MCP server 要先做 read_file,其餘下週。
[todo]
1. 寫完 Day 7
2. 測 streamable http
3. 補 pytest
[shopping]
明天要買咖啡豆
但這次不是模型自己呼叫 notes://all。
而是 Host / Client 主動執行:
resources/read
取得內容之後,再由應用程式決定怎麼使用,例如顯示給使用者,或放進模型的 context。
所以 Resource 比較像 Server 對外提供的「資料來源」。
Server 負責提供資料,應用程式決定什麼時候讀。
這就是 Resources 和 Tools 最重要的差別。
不是。
這是我一開始最容易搞混的地方。
例如 notes://all 完全可以改成:
@mcp.tool()
def list_notes():
...
這樣模型就能自己判斷什麼時候需要查筆記。
所以不能簡單理解成:
會修改資料 → Tool
只讀資料 → Resource
Tool 本身也可以是唯讀操作。
例如:
這些都沒有修改資料,但如果希望 模型自己決定什麼時候需要查,通常就適合設計成 Tool。
相反地,像:
如果希望由應用程式決定是否讀取或載入,就比較適合 Resource。
所以我的判斷方式會是:
模型決定要不要呼叫 → Tool
Host / Client 決定要不要讀取 → Resource
使用者主動選擇 → Prompt
不是看它「讀還是寫」,而是看 控制權在哪裡。
Prompt 又是另一種角色。
這次 Server 提供:
summarize_notes(style="條列")
取得後實際會得到一段提示內容:
請閱讀我所有的筆記,並用「條列」的方式摘要重點。
只講結論,不要重複原文。用繁體中文回答。
它本身不是讓模型自行決定是否呼叫的工具,而是一個 Server 提供的可重複任務模板。
支援 MCP Prompts 的 Client 可以把這些 Prompt 做成 UI、選單或指令,讓使用者主動觸發。
例如:
摘要我的筆記
產生 Code Review Prompt
建立除錯流程
整理會議內容
這類固定而且會重複使用的流程,就很適合包成 Prompt。
所以 Prompts 的控制權主要在 使用者。
把三個放在一起看會更明顯:
# Tool
await session.call_tool(
"now",
{"tz": "Asia/Taipei"}
)
# Resource
await session.read_resource(
"notes://todo"
)
# Prompt
await session.get_prompt(
"summarize_notes",
{"style": "條列"}
)
雖然都是 Server 提供的能力,但 MCP 從協定層就把它們分成三套不同的介面。
這也是為什麼設計 Server 時,不應該把所有東西全部塞進 Tools。
到目前為止講的 Tools、Resources、Prompts,方向基本上都是:
Server → Client
也就是 Server 告訴 Client:
我有哪些能力可以提供。
但 MCP 不只有這個方向。
Client 也能提供能力給 Server。
例如 Sampling。
Server 可以透過 Client 請求一次模型生成,而不是自己另外直接整合一套模型 API。
另外還有 Elicitation。
Server 執行任務到一半,如果缺少必要資訊,可以透過 Client 向使用者要求輸入。
還有 Roots,Client 可以告訴 Server目前有哪些檔案系統範圍可供操作。
不過 Roots 比較像是「範圍宣告」,不能單獨當成真正的安全邊界。
真正要限制一個 Server 能碰哪些檔案,還是要搭配 sandbox、OS 權限或其他安全機制。
所以昨天畫的 Host / Client / Server 三角關係,到這裡又多了一塊:
Server → Client
Tools
Resources
Prompts
Client → Server
Sampling
Elicitation
Roots
MCP 並不是單純的「Client 呼叫 Server」,而是一套雙向協作的協定。
這次實作碰到兩個值得記一下的地方。
resources/list像這種:
@mcp.resource("notes://{key}")
URI 裡面有參數的 Resource,屬於 Resource Template。
所以它不會出現在:
resources/list
而是在:
resources/templates/list
第一次只看 resources/list,很容易以為 Resource 根本沒有成功註冊。
例如把所有筆記操作全部塞進:
manage_notes(
action="add" | "delete" | "list"
)
看起來只需要維護一個 Tool,但模型除了要先判斷要不要使用 manage_notes,還要再判斷 action 應該填什麼。
通常拆成:
add_note
delete_note
list_notes
會更清楚。
Tool 名稱、用途和參數越明確,模型越容易選對。
昨天知道了 MCP 裡 Host、Client、Server 各自負責什麼。
今天則往 Server 裡面再拆一層:
Tools → 模型控制
Resources → 應用程式控制
Prompts → 使用者控制
實際設計時,不用先糾結「它是讀資料還是寫資料」。
先問:
我希望誰決定什麼時候使用它?
答案通常就會很清楚。
不過目前為止,我們其實都還躲在 SDK 後面。
像:
session.initialize()
session.list_tools()
session.call_tool()
session.read_resource()
看起來只是幾個 Python method,但底下到底傳了什麼,現在還沒有真正看過。
明天把 SDK 拿掉。
從 JSON-RPC 2.0 開始,手寫一次 MCP 的訊息生命週期,把 initialize、request、response 到連線建立的過程全部攤開來看。